package com.github.zyf.ioaj.export;

import com.github.zyf.ioaj.algorithm.AbstractAlgorithm;
import com.github.zyf.ioaj.constant.ParamType;
import com.github.zyf.ioaj.annotation.internal.*;
import com.github.zyf.ioaj.exception.AlgorithmClassParserException;
import com.github.zyf.ioaj.exception.NotLoadedException;
import com.github.zyf.ioaj.exception.constant.AlgorithmClassParserExceptionType;
import com.github.zyf.ioaj.utils.config.ExportFileConfiguration;
import com.github.zyf.ioaj.constant.FileType;

import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.Method;

/**
 * <h1>导出抽象类</h1>
 * <p>
 * 用于导出算法计算相关信息，不过这是一个抽象类，仅仅提供了一些通用方法：
 * <ul>
 *     <li>扫描{@link BasicParam}注解的字段：{@link #scanBasicParam(AbstractAlgorithm, Field, ContentBuilder)}</li>
 *     <li>扫描{@link TempParam}注解的字段：{@link #scanTempParam(AbstractAlgorithm, Field, ContentBuilder)}</li>
 *     <li>扫描{@link InputParam}注解的字段：{@link #scanInputParam(AbstractAlgorithm, Field, ContentBuilder)}</li>
 *     <li>扫描{@link OutputParam}注解的字段：{@link #scanOutputParam(AbstractAlgorithm, Field, ContentBuilder)}</li>
 * </ul>
 * 这些方法都是protected权限的
 * <p>
 * 此外，该方法的构造函数需要5个必须参数
 * <ul>
 *     <li>算法运算器：{@link #algorithm}</li>
 *     <li>作者：{@link #author}</li>
 *     <li>备注：{@link #message}</li>
 *     <li>标题：{@link #title}</li>
 *     <li>文件类型：{@link #fileType}</li>
 * </ul>
 * <p>
 * 对于使用者而言，只需要关注下面这些方法
 * <ul>
 *     <li>导出到文件：{@link #exportToFile(String)}，不过要在加载完成后才能进行导出到文件</li>
 *     <li>导出字符串：{@link #export()}，不过该方法由各子类具体实现</li>
 * </ul>
 * 注：逻辑上{@link #exportToFile(String)}调用了{@link #export()}方法，
 * 因此使用者在使用{@link #exportToFile(String)}方法时，无需关心导出状态
 * <p>注：保证所有参数属性在添加对应的注解之前，实现了对应的get方法（普通变量的get方法get开头，布尔型变量的get方法is开头）</p>
 *
 * @author zyf
 */
public abstract class AbstractExport {

    /**
     * 算法运算器
     */
    private final AbstractAlgorithm algorithm;

    /**
     * 作者
     */
    private String author;
    /**
     * 备注
     */
    private String message;
    /**
     * 标题
     */
    private String title;
    /**
     * 导出文件类型
     */
    private FileType fileType;

    /**
     * 报告是否加载完成
     */
    private boolean isLoaded;

    protected void setLoaded() {
        isLoaded = true;
    }

    public AbstractExport(AbstractAlgorithm algorithm, String author, String message, String title, FileType fileType) {
        this.algorithm = algorithm;
        this.author = author;
        this.message = message;
        this.title = title;
        this.fileType = fileType;
    }

    /**
     * 将报告解析并导出到文件
     *
     * @param filename 文件名
     * @return 生成的文件所在位置
     */
    public String exportToFile(String filename) {
        if (!isLoaded) throw new NotLoadedException("报告未加载完成");
        String rootP = ExportFileConfiguration.exportRootPath;
        // 创建根路径文件夹
        File folder = new File(rootP);
        folder.mkdirs();
        String pathname;
        // 如果文件名为空，设置文件名为时间戳
        if (filename.isEmpty()) pathname = ExportFileConfiguration.pathWithTime(fileType);
        else pathname = rootP + filename + fileType.suffix();
        File file = new File(pathname);
        FileWriter fileWriter;
        BufferedWriter bufferedWriter;
        try {
            boolean isExist = file.createNewFile();
            // 如果文件已存在，则修改文件名为${filename}_${currentTimeMillis}
            if (!isExist) {
                file = new File(ExportFileConfiguration.pathWithTime(filename, fileType));
                file.createNewFile();
            }
            fileWriter = new FileWriter(file);
            bufferedWriter = new BufferedWriter(fileWriter);
            String content;
            // 根据文件类型写入对应的内容
            content = export();
            bufferedWriter.write(content);
            bufferedWriter.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return file.getAbsolutePath();
    }

    /**
     * 将报告输出为字符串
     *
     * @return {@link #fileType} 格式字符串
     */
    public abstract String export();

    /**
     * 扫描{@link BasicParam}注解的参数
     *
     * @param algorithm      算法类
     * @param field          字段
     * @param contentBuilder 内容加载器
     * @return 是否扫描成功
     */
    protected boolean scanBasicParam(AbstractAlgorithm algorithm, Field field, ContentBuilder contentBuilder) {
        if (field.isAnnotationPresent(BasicParam.class)) {
            // 如果检测到了，那么就设置isAnnotated为true
            // isAnnotated = true;
            // 获取对应的注解
            BasicParam annotation = field.getAnnotation(BasicParam.class);
            // 获取参数变量类型
            ParamType paramType = annotation.type();
            // 获取参数变量含义描述
            String desc = annotation.desc();
            // 获取字段名，作为键来使用
            String key = field.getName();
            // 调用get方法获取字段值
            Object value = getValue(algorithm, key);
            // 如果值为空，将抛出异常
            scanClassField(contentBuilder, paramType, key, desc, value);
            return true;
        }
        return false;
    }

    /**
     * 扫描{@link TempParam}注解的参数
     *
     * @param algorithm      算法类
     * @param field          字段
     * @param contentBuilder 内容加载器
     * @return 是否扫描成功
     */
    protected boolean scanTempParam(AbstractAlgorithm algorithm, Field field, ContentBuilder contentBuilder) {
        if (field.isAnnotationPresent(TempParam.class)) {
            // 如果检测到了，那么就设置isAnnotated为true
            // isAnnotated = true;
            // 获取对应的注解
            TempParam annotation = field.getAnnotation(TempParam.class);
            // 获取参数变量类型
            ParamType paramType = annotation.type();
            // 获取参数变量含义描述
            String desc = annotation.desc();
            // 获取字段名，作为键来使用
            String key = field.getName();
            // 调用get方法获取字段值
            Object value = getValue(algorithm, key);
            // 如果值为空，将抛出异常
            scanClassField(contentBuilder, paramType, key, desc, value);
            return true;
        }
        return false;
    }

    /**
     * 扫描{@link InputParam}注解的参数
     *
     * @param algorithm      算法类
     * @param field          字段
     * @param contentBuilder 内容加载器
     * @return 是否扫描成功
     */
    protected boolean scanInputParam(AbstractAlgorithm algorithm, Field field, ContentBuilder contentBuilder) {
        if (field.isAnnotationPresent(InputParam.class)) {
            // 如果检测到了，那么就设置isAnnotated为true
            // isAnnotated = true;
            // 获取对应的注解
            InputParam annotation = field.getAnnotation(InputParam.class);
            // 获取参数变量类型
            ParamType paramType = annotation.type();
            // 获取参数变量含义描述
            String desc = annotation.desc();
            // 获取字段名，作为键来使用
            String key = field.getName();
            // 调用get方法获取字段值
            Object value = getValue(algorithm, key);
            // 如果值为空，将抛出异常
            scanClassField(contentBuilder, paramType, key, desc, value);
            return true;
        }
        return false;
    }

    /**
     * 扫描{@link OutputParam}注解的参数
     *
     * @param algorithm      算法类
     * @param field          字段
     * @param contentBuilder 内容加载器
     * @return 是否扫描成功
     */
    protected boolean scanOutputParam(AbstractAlgorithm algorithm, Field field, ContentBuilder contentBuilder) {
        if (field.isAnnotationPresent(OutputParam.class)) {
            // 如果检测到了，那么就设置isAnnotated为true
            // isAnnotated = true;
            // 获取对应的注解
            OutputParam annotation = field.getAnnotation(OutputParam.class);
            // 获取参数变量类型
            ParamType paramType = annotation.type();
            // 获取参数变量含义描述
            String desc = annotation.desc();
            // 获取字段名，作为键来使用
            String key = field.getName();
            // 调用get方法获取字段值
            Object value = getValue(algorithm, key);
            // 如果值为空，将抛出异常
            scanClassField(contentBuilder, paramType, key, desc, value);
            return true;
        }
        return false;
    }

    /**
     * 扫描当前类的字段
     *
     * @param builder   内容加载器
     * @param paramType 参数类型
     * @param key       键
     * @param desc      描述
     * @param value     值
     */
    private void scanClassField(ContentBuilder builder, ParamType paramType, String key, String desc, Object value) {
        // 如果值为空，将抛出异常
        if (value == null) {
            throw new AlgorithmClassParserException(AlgorithmClassParserExceptionType.ALGORITHM_PARAM_VALUE_NULL);
        }
        // 处理参数信息
        parseParam(builder, paramType, key, desc, value);
    }

    /**
     * 扫描对象，如果参数类型是对象，则需要对其进行递归扫描
     *
     * @param object 参数对象
     */
    private void scanObject(Object object, ContentBuilder builder) {
        // 获取对象的类
        Class<?> clazz = object.getClass();
        // 判断对象的类是否添加了@Param注解
        if (!clazz.isAnnotationPresent(Param.class)) {
            throw new AlgorithmClassParserException(AlgorithmClassParserExceptionType.ALGORITHM_PARAM_OBJECT_NOT_ANNOTATED);
        }
        boolean isAnnotated = false;
        for (Field field : clazz.getDeclaredFields()) {
            // 允许字段在访问时可以被检查
            field.setAccessible(true);
            // 判断字段是否添加了@ParamItem注解
            if (field.isAnnotationPresent(ParamItem.class)) {
                // 如果检测到了，那么就设置isAnnotated为true
                isAnnotated = true;
                // 获取对应的注解
                ParamItem annotation = field.getAnnotation(ParamItem.class);
                // 获取参数变量类型
                ParamType paramType = annotation.type();
                // 获取参数变量含义描述
                String desc = annotation.desc();
                // 获取字段名，作为键来使用
                String key = field.getName();
                // 调用get方法获取字段值
                Object value = getValue(object, key);
                // 如果值为空，将抛出异常
                if (value == null) {
                    throw new AlgorithmClassParserException(AlgorithmClassParserExceptionType.ALGORITHM_PARAM_VALUE_NULL);
                }
                // 按照参数变量的类型进行相应的处理
                parseParam(builder, paramType, key, desc, value);
            }
        }
        if (!isAnnotated) {
            throw new AlgorithmClassParserException(AlgorithmClassParserExceptionType.ALGORITHM_PARAM_OBJECT_FIELD_NOT_ANNOTATED);
        }
    }

    /**
     * 根据参数类型解析参数信息
     *
     * @param builder   内容加载器
     * @param paramType 参数类型
     * @param key       键
     * @param desc      描述
     * @param value     值
     */
    private void parseParam(ContentBuilder builder, ParamType paramType, String key, String desc, Object value) {
        switch (paramType) {
            case STRING:
                builder.appendString(key, desc, value.toString());
                break;
            case STRING_ARRAY_1:
                builder.appendStrings_1(key, desc, (String[]) value);
                break;
            case STRING_ARRAY_2:
                builder.appendStrings_2(key, desc, (String[][]) value);
                break;
            case BOOL:
                builder.appendBoolean(key, desc, (boolean) value);
                break;
            case INT:
                builder.appendInt(key, desc, (int) value);
                break;
            case INT_ARRAY_1:
                builder.appendInts_1(key, desc, (int[]) value);
                break;
            case INT_ARRAY_2:
                builder.appendInts_2(key, desc, (int[][]) value);
                break;
            case DOUBLE:
                builder.appendDouble(key, desc, (double) value);
                break;
            case DOUBLE_ARRAY_1:
                builder.appendDoubles_1(key, desc, (double[]) value);
                break;
            case DOUBLE_ARRAY_2:
                builder.appendDoubles_2(key, desc, (double[][]) value);
                break;
            case OBJECT:
                builder.appendObjectHeader(key, desc);
                scanObject(value, builder);
                builder.appendObjectEnd(value);
                break;
        }
    }

    /**
     * 动态获取字段值
     *
     * @param object 类对象
     * @param name   字段名
     * @return Object
     */
    private static Object getValue(Object object, String name) {
        Class<?> clazz = object.getClass();
        try {
            Method[] m = clazz.getMethods();
            for (Method method : m) {
                if (("get" + name).equalsIgnoreCase(method.getName())) {
                    return method.invoke(object);
                } else if (("is" + name).equalsIgnoreCase(method.getName())) {
                    return method.invoke(object);
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    public String getAuthor() {
        return author;
    }

    public void setAuthor(String author) {
        this.author = author;
    }

    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }

    public AbstractAlgorithm getAlgorithm() {
        return algorithm;
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public FileType getFileType() {
        return fileType;
    }

    public void setFileType(FileType fileType) {
        this.fileType = fileType;
    }
}
